// SPDX-License-Identifier: GPL-2.0
/* smc-dev.c - Driver of LomboTech SMC module
 *
 * Copyright (C) 2016-2018, LomboTech Co.Ltd.
 * Author: lomboswer <lomboswer@lombotech.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 */


#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/poll.h>
#include <linux/sched/signal.h>
#include <mach/debug.h>
#include "smc.h"
#include "uapi/smc.h"

#define SMC_DEV_MAX 1
#define SMC_DEV_BUSY 0

static dev_t smc_devt;
struct class *smc_class;
static DEFINE_IDA(smc_ida);


static void smc_device_release(struct device *dev)
{
	struct smc_device *smc = to_smc_device(dev);

	ida_simple_remove(&smc_ida, smc->id);
	kfree(smc);
}

/* allocate device */
static struct smc_device *smc_allocate_device(void)
{
	struct smc_device *smc;

	smc = kzalloc(sizeof(*smc), GFP_KERNEL);
	if (!smc)
		return NULL;

	device_initialize(&smc->dev);

	/* Drivers can revise this default after allocating the device. */

	smc->irq_freq = 1;
	smc->dev.class = smc_class;
	smc->dev.groups = smc_get_dev_attribute_groups();
	smc->dev.release = smc_device_release;

	mutex_init(&smc->ops_lock);
	spin_lock_init(&smc->irq_lock);
	init_waitqueue_head(&smc->irq_queue);

	return smc;
}

/* device unregister */
void smc_device_unregister(struct smc_device *smc)
{
	mutex_lock(&smc->ops_lock);
	cdev_device_del(&smc->char_dev, &smc->dev);
	smc->ops = NULL;
	mutex_unlock(&smc->ops_lock);
	put_device(&smc->dev);
}

/* device register */
struct smc_device *smc_device_register(const char *name, struct device *dev,
			const struct smc_class_ops *ops, struct module *owner)
{
	struct smc_device *smc;
	int id, err;

	id = ida_simple_get(&smc_ida, 0, 0, GFP_KERNEL);
	if (id < 0) {
		err = id;
		goto exit;
	}

	smc = smc_allocate_device();
	if (!smc) {
		err = -ENOMEM;
		goto exit_ida;
	}

	PRT_INFO("ida_simple_get id is %d\n", id);

	PRT_INFO("major is %d, minor is %d\n",
		 MAJOR(smc_devt), MINOR(smc_devt));
	smc->id = id;
	smc->ops = ops;
	smc->owner = owner;
	smc->dev.parent = dev;

	strlcpy(smc->name, name, SMC_DEVICE_NAME_SIZE);
	dev_set_name(&smc->dev, "smc%d", id);

	smc_dev_prepare(smc);

	err = cdev_device_add(&smc->char_dev, &smc->dev);
	if (err) {
		dev_warn(&smc->dev, "%s: failed to add char device %d:%d\n",
			 name, MAJOR(smc->dev.devt), smc->id);
		/* This will free both memory and the ID */
		put_device(&smc->dev);
		goto exit;
	} else {
		dev_dbg(&smc->dev, "%s: dev (%d:%d)\n", name,
			MAJOR(smc->dev.devt), smc->id);
	}

	smc->registered = true;
	dev_info(dev, "smc core: registered %s as %s\n",
			name, dev_name(&smc->dev));
	return smc;

exit_ida:
	ida_simple_remove(&smc_ida, id);

exit:
	dev_err(dev, "smc core: unable to register %s, err = %d\n", name, err);
	return ERR_PTR(err);

}

/* open device */
static int smc_dev_open(struct inode *inode, struct file *file)
{
	struct smc_device *smc = container_of(inode->i_cdev,
					struct smc_device, char_dev);

	if (test_and_set_bit_lock(SMC_DEV_BUSY, &smc->flags))
		return -EBUSY;

	file->private_data = smc;

	spin_lock_irq(&smc->irq_lock);
	smc->irq_data = 0;
	spin_unlock_irq(&smc->irq_lock);

	return 0;
}

/* device ioctl */
static long smc_dev_ioctl(struct file *file,
		unsigned int cmd, unsigned long arg)
{
	int err = 0;
	struct smc_device *smc = file->private_data;
	void __user *uarg = (void __user *) arg;

	err = mutex_lock_interruptible(&smc->ops_lock);
	if (err)
		return err;

	PRT_INFO("smc dev ioctl start\n");
	switch (cmd) {
	case SMC_RD_SPECAIL_OPERATION:
		mutex_unlock(&smc->ops_lock);

		err = smc_read_specail_operation();
		if (err < 0)
			return err;

		if (copy_to_user(uarg, &err, sizeof(err)))
			return -EFAULT;
		return 0;

	case SMC_SET_SPECAIL_OPERATION:
		mutex_unlock(&smc->ops_lock);

		if (copy_from_user(&err, uarg, sizeof(err)))
			return -EFAULT;

		return smc_set_specail_operation(err);

	case SMC_RD_TRAN_MODE:
		mutex_unlock(&smc->ops_lock);

		err = smc_read_transfer_mode();
		if (err < 0)
			return err;

		if (copy_to_user(uarg, &err, sizeof(err)))
			return -EFAULT;
		return 0;

	case SMC_SET_TRAN_MODE:
		mutex_unlock(&smc->ops_lock);

		if (copy_from_user(&err, uarg, sizeof(err)))
			return -EFAULT;

		return smc_set_transfer_mode(err);

	case SMC_RD_STOP_CLK_EN:
		mutex_unlock(&smc->ops_lock);

		err = smc_read_stop_clk_enable();
		if (err < 0)
			return err;

		if (copy_to_user(uarg, &err, sizeof(err)))
			return -EFAULT;
		return 0;

	case SMC_SET_STOP_CLK_EN:
		mutex_unlock(&smc->ops_lock);

		if (copy_from_user(&err, uarg, sizeof(err)))
			return -EFAULT;

		return smc_set_stop_clk_enable(err);

	case SMC_RD_BLOCK_EDC_TYPE:
		mutex_unlock(&smc->ops_lock);

		err = smc_read_block_edc_type();
		if (err < 0)
			return err;

		if (copy_to_user(uarg, &err, sizeof(err)))
			return -EFAULT;
		return 0;

	case SMC_SET_BLOCK_EDC_TYPE:
		mutex_unlock(&smc->ops_lock);

		if (copy_from_user(&err, uarg, sizeof(err)))
			return -EFAULT;

		return smc_set_block_edc_type(err);

	case SMC_RD_START_STATUS:
		mutex_unlock(&smc->ops_lock);

		err = smc_read_start_status();
		if (err < 0)
			return err;

		if (copy_to_user(uarg, &err, sizeof(err)))
			return -EFAULT;
		return 0;
	}
	return err;
}

ssize_t smc_dev_write(struct file *file, const char __user *user,
		      size_t size, loff_t *l)
{
	return 0;
}

/* read device interface */
ssize_t smc_dev_read(struct file *file, char __user *buf,
		     size_t count, loff_t *ppos)
{
	struct smc_device *smc = file->private_data;

	DECLARE_WAITQUEUE(wait, current);
	unsigned long data;
	ssize_t ret;

	if (count != sizeof(unsigned int) && count < sizeof(unsigned long))
		return -EINVAL;

	add_wait_queue(&smc->irq_queue, &wait);
	do {
		__set_current_state(TASK_INTERRUPTIBLE);

		spin_lock_irq(&smc->irq_lock);
		data = smc->irq_data;
		smc->irq_data = 0;
		spin_unlock_irq(&smc->irq_lock);

		if (data != 0) {
			ret = 0;
			break;
		}
		if (file->f_flags & O_NONBLOCK) {
			ret = -EAGAIN;
			break;
		}
		if (signal_pending(current)) {
			ret = -ERESTARTSYS;
			break;
		}
		schedule();
	} while (1);
	set_current_state(TASK_RUNNING);
	remove_wait_queue(&smc->irq_queue, &wait);

	if (ret == 0) {
		/* Check for any data updates */
		if (smc->ops->read_callback)
			data = smc->ops->read_callback(smc->dev.parent, data);

		if (sizeof(int) != sizeof(long) &&
		    count == sizeof(unsigned int))
			ret = put_user(data, (unsigned int __user *)buf) ?:
				sizeof(unsigned int);
		else
			ret = put_user(data, (unsigned long __user *)buf) ?:
				sizeof(unsigned long);
	}
	return ret;
}

static __poll_t smc_dev_poll(struct file *file, struct poll_table_struct *wait)
{
	struct smc_device *smc = file->private_data;
	unsigned long data;

	poll_wait(file, &smc->irq_queue, wait);

	data = smc->irq_data;

	return (data != 0) ? (EPOLLIN | EPOLLRDNORM) : 0;

}

int smc_dev_release(struct inode *inode, struct file *file)
{
	struct smc_device *smc = file->private_data;

	clear_bit_unlock(SMC_DEV_BUSY, &smc->flags);
	return 0;
}

static int smc_dev_fasync(int fd, struct file *file, int on)
{
	struct smc_device *smc = file->private_data;

	return fasync_helper(fd, file, on, &smc->async_queue);
}

static const struct file_operations smc_dev_fops = {
	.owner		= THIS_MODULE,
	.llseek		= no_llseek,
	.read		= smc_dev_read,
	.poll		= smc_dev_poll,
	.unlocked_ioctl	= smc_dev_ioctl,
	.open		= smc_dev_open,
	.release	= smc_dev_release,
	.fasync		= smc_dev_fasync,
};

void smc_dev_prepare(struct smc_device *smc)
{
	PRT_INFO("start smc dev prepare\n");
	if (!smc_devt)
		return;
	smc->dev.devt = MKDEV(MAJOR(smc_devt), smc->id);
	cdev_init(&smc->char_dev, &smc_dev_fops);
	smc->char_dev.owner = smc->owner;
}

void smc_dev_add_device(struct smc_device *smc)
{
	PRT_INFO("start smc dev add device\n");
	if (cdev_add(&smc->char_dev, smc->dev.devt, 1))
		dev_warn(&smc->dev, "%s: failed to add char device %d:%d\n",
			smc->name, MAJOR(smc_devt), smc->id);
	else
		dev_dbg(&smc->dev, "%s: dev (%d:%d)\n", smc->name,
			MAJOR(smc_devt), smc->id);
}

void smc_dev_del_device(struct smc_device *smc)
{
	if (smc->dev.devt)
		cdev_del(&smc->char_dev);
}


void __init smc_dev_init(void)
{
	int err;

	err = alloc_chrdev_region(&smc_devt, 0, SMC_DEV_MAX, "smc");

	pr_err("major is %d, minor is %d\n", MAJOR(smc_devt), MINOR(smc_devt));
	if (err < 0)
		pr_err("failed to allocate char dev region\n");
}

void __exit smc_dev_exit(void)
{
	if (smc_devt)
		unregister_chrdev_region(smc_devt, SMC_DEV_MAX);
}

static int __init smc_class_init(void)
{
	smc_class = class_create(THIS_MODULE, "smc");
	if (IS_ERR(smc_class)) {
		pr_err("couldn't create class\n");
		return PTR_ERR(smc_class);
	}
	smc_dev_init();
	return 0;
}
subsys_initcall(smc_class_init);

